/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.openjaws.plugins;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

import javax.annotation.PostConstruct;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import au.com.lastweekend.openjaws.api.Readings;
import au.com.lastweekend.openjaws.api.WeatherPluginException;
import au.com.lastweekend.openjaws.units.PressureUnit;
import au.com.lastweekend.openjaws.units.RainfallUnit;
import au.com.lastweekend.openjaws.units.TemperatureUnit;
import au.com.lastweekend.openjaws.units.WindUnit;
import au.com.lastweekend.openjaws.util.StringUtil;

/**
 * Push each reading to the weather underground website.
 * 
 * @author ggardner
 * 
 */
public class WeatherUndergroundPlugin extends AbstractWeatherPlugin {

    private static final Logger LOG = LoggerFactory.getLogger(WeatherUndergroundPlugin.class);

    private static final String DEFAULT_SOFTWARE_TYPE = "OpenJaWS";
    private static final String DEFAULT_UPLOAD_URI = "http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php";
    private static final String DEFAULT_CURRENT_OBSERVATION_URI = "http://api.wunderground.com/weatherstation/WXCurrentObXML.asp";

    private HttpClient httpClient;

    private String id = null;
    private String password = null;
    private String uploadURI = DEFAULT_UPLOAD_URI;
    private String currentObservationURI = DEFAULT_CURRENT_OBSERVATION_URI;

    private int timeout = 5000;

    private String softwareType = DEFAULT_SOFTWARE_TYPE;

    public WeatherUndergroundPlugin(HttpClient httpClient) {

        this.httpClient = httpClient;

    }

    @PostConstruct
    public void init() {

        if (isEnabled()) {
            HttpClientParams params = this.httpClient.getParams();
            params.setSoTimeout(timeout);
            this.httpClient.setParams(params);
            LOG.info(String.format("Enabled, station id=%s, timeout=%d ms", getId(), getTimeout()));
            LOG.debug("Updating to " + uploadURI);

        } else {
            LOG.info("Disabled");
        }
    }

    public void processReadings(Readings readings) throws WeatherPluginException {

        HttpMethod method = new GetMethod(uploadURI);
        Map<String, String> params = new HashMap<String, String>(15);

        populateParams(readings, params);

        NameValuePair[] queryParams = toNameValuePairArray(params);

        try {
            method.setQueryString(queryParams);
            if (LOG.isDebugEnabled()) {
                LOG.debug(method.getURI().toString());
            }

            int response = httpClient.executeMethod(method);
            String responseBody = method.getResponseBodyAsString();

            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("WeatherUnderground http response %d\n%s", response, responseBody));
            }

            if (response != 200 || responseBody == null || !responseBody.contains("success")) {
                throw new WeatherPluginException("Bad response (" + response + ") " + responseBody == null ? "null" : responseBody);
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Successful update for %tc", readings.getReadingTime()));
            }

        } catch (IOException e) {
            throw new WeatherPluginException("Error updating weatherUnderground", e);
        } finally {
            method.releaseConnection();
        }

    }

    public boolean isEnabled() {

        return super.isEnabled() && !StringUtil.isEmpty(id);
    }

    protected void populateParams(Readings readings, Map<String, String> params) {

        addParam(params, "action", "updateraw");
        addParam(params, "ID", id);
        addParam(params, "PASSWORD", password);
        addParam(params, "softwaretype", getSoftwareType());
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        df.setTimeZone(TimeZone.getTimeZone("UTC"));
        addParam(params, "dateutc", df.format(readings.getReadingTime()));

        addParam(params, "windspeedmph", readings.getWindSpeed().getCurrent().convertTo(WindUnit.MILES_PER_HOUR).getValue(),
                "%.2f");
        addParam(params, "winddir", readings.getWindSpeed().getCurrent().getDirection(), "%.2f");
        /*
         * TODO: Average wind info. wind gust, using software specific time period] windgustdir - [0-360 using software specific
         * time period] windspdmph_avg2m - [mph 2 minute average wind speed mph] winddir_avg2m - [0-360 2 minute average wind
         * direction] windgustmph_10m - [mph past 10 minutes wind gust mph ] windgustdir_10m - [0-360 past 10 minutes wind gust
         * direction]
         */

        addParam(params, "humidity", readings.getOutdoorHumidity().getCurrent().getValue(), "%.0f");
        addParam(params, "tempf", readings.getOutdoorTemperature().getCurrent().convertTo(TemperatureUnit.FAHRENHEIT).getValue(),
                "%.2f");

        // for extra outdoor sensors use temp2f, temp3f, and so on

        addParam(params, "rainin", readings.getRain1H().getCurrent().convertTo(RainfallUnit.INCH).getValue(), "%.3f");
        // TODO dailyrainin - [rain inches so far today in local time]

        addParam(params, "baromin", readings.getAirPressure().getCurrent().convertTo(PressureUnit.INCHES_OF_MERCURY).getValue(),
                "%.2f");

        addParam(params, "indoorhumidity", readings.getIndoorHumidity().getCurrent().getValue(), "%.0f");
        addParam(params, "indoortempf", readings.getIndoorTemperature().getCurrent().convertTo(TemperatureUnit.FAHRENHEIT)
                .getValue(), "%.2f");

        /*
         * rapidfire doesn't really look any different // &realtime=1&rtfreq=2.5 addParam(params, "realtime", "1");
         * addParam(params, "rtfreq=", 3.0, "%.1f");
         */
    }

    private static void addParam(Map<String, String> params, String name, Double value, String format) {

        if (value != null) {
            addParam(params, name, String.format(format, value));
        }

    }

    private static void addParam(Map<String, String> params, String name, String value) {

        if (value != null) {
            params.put(name, value);
        }

    }

    private static NameValuePair[] toNameValuePairArray(Map<String, String> params) {

        NameValuePair[] queryParams = new NameValuePair[params.size()];

        int i = 0;
        for (String name : params.keySet()) {
            queryParams[i++] = new NameValuePair(name, params.get(name));
        }
        return queryParams;
    }

    public String getSoftwareType() {

        return softwareType;
    }

    public void setSoftwareType(String softwareType) {

        this.softwareType = softwareType;
    }

    /**
     * This would be a good idea except that the api doesn't use the date_utc supplied by the station rather it uses the time at
     * which the server receives the update, which is not very predictable
     * 
     * and sometimes is stops updating altogether
     * 
     * @return the last observation date or null if some problem occurs
     */
    @Override
    protected Date getLastReadingTime() {

        GetMethod method = new GetMethod(currentObservationURI);
        method.setQueryString("ID=" + getId());
        Date result = null;
        try {
            int response = httpClient.executeMethod(method);

            if (response != 200) {
                LOG.warn(String.format("Unexpected response %d from %s", response, currentObservationURI));
            } else {
                DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
                InputStream responseBodyAsStream = method.getResponseBodyAsStream();

                Document document = builder.parse(responseBodyAsStream);

                if (LOG.isDebugEnabled()) {
                    // print
                    StringWriter writer = new StringWriter();
                    writer.append("Response output/n");

                    Transformer tr = TransformerFactory.newInstance().newTransformer();
                    tr.setOutputProperty(OutputKeys.INDENT, "yes");
                    tr.setOutputProperty(OutputKeys.METHOD, "xml");
                    tr.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");

                    // to send the output to a file
                    tr.transform(new DOMSource(document), new StreamResult(writer));
                    LOG.debug(writer.toString());

                }

                Element root = document.getDocumentElement();
                NodeList nodes = root.getElementsByTagName("observation_time_rfc822");
                if (nodes.getLength() > 0) {
                    String obsTimeRFC822 = nodes.item(0).getTextContent();
                    result = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z").parse(obsTimeRFC822);
                    LOG.info(String.format("Last observation time %tc, from %s", result, currentObservationURI));
                }
            }
        } catch (Exception e) {
            LOG.error("Failed to retrieve last observation time from " + currentObservationURI, e);
        } finally {
            method.releaseConnection();
        }

        return result;
    }

    public String getUri() {

        return uploadURI;
    }

    public void setUri(String wuURI) {

        this.uploadURI = wuURI;
    }

    public String getId() {

        return id;
    }

    public void setId(String wuID) {

        this.id = wuID;
    }

    public String getPassword() {

        return password;
    }

    public void setPassword(String wuPassword) {

        this.password = wuPassword;
    }

    public int getTimeout() {

        return timeout;
    }

    public void setTimeout(int timeout) {

        this.timeout = timeout;
    }
}
